'''
Provides logging capabilities for SALSA Service Platform
Unlike logging module from standart library, it looks more like syslog

@author: myaut
'''

import sys
import datetime
import re
import os
import threading

from Queue import Queue, Empty

from salsa.util.io import IOSlave, RPCEncoderDecoder, IOSlaveDead, IOException, IOTransactionError
from salsa.util.rpc import RPCProtocol
from salsa.util import LoggerSeverity

from threading import current_thread

import salsa
from salsa import trace_log, do_trace, SALSAException

LOG_QUEUE_TIMEOUT = 0.1
LOG_QUEUE_SIZE = 512

class LoggerException(SALSAException):
    '''Derived from Exception class
    Thrown if exception occured in client or service'''
    pass

class LogDestination:
    '''Receives log messages from main logger service and passes them into destinations'''
    
    def __init__(self, dest, severity, source, append):
        '''Constructs new LogDestination object:
            @param destination : may be @STDERR or relative filename (logs are written into data directory)
            @param severity : minimum severity of message that pass to this destination
            @param source : regex for source
            @param append : (bool) append or rewrite log (for files only)'''
        self.severity = LoggerSeverity(severity)
        self.source_re = re.compile(source)
        
        self.dest = dest
        
        self.lock = threading.Semaphore()
        
        if dest == '@STDERR':
            self.out = sys.stderr
        else:
            self.open_dest_file(salsa.core.abs_data_path(dest), bool(append))
    
    def open_dest_file(self, dest, append):
        '''Helper for __init__ that opens destination if it is a file'''
        try:
            dest_dir = os.path.dirname(dest)
            
            if not dest_dir and not os.path.isabs(dest) and not os.path.exists(dest_dir):
                os.makedirs(dest_dir)
            
            if not os.path.exists(dest):
                append = False
            
            self.out = open(dest, 'a' if append else 'w')
        except IOError as ioerr:
            raise LoggerException('Wrong logger destination %s: error %s' % (dest, str(ioerr)))
    
    def log(self, source, tname, severity, message):
        '''Main log function
        
        @todo alternate formatters''' 
        try:
            self.lock.acquire()
            
            if self.source_re.match(source) and severity >= self.severity:
                now = datetime.datetime.now().strftime('%b %d %H:%M:%S,%f')
                
                print >> self.out, '%s %s@%s [%s] %s' % (now, source, tname, severity, message)
                self.out.flush()
        finally:
            self.lock.release()
            

class LoggerService:
    '''Core logger service'''
    def __init__(self):        
        self.config()
    
    def config(self):   
        '''Helper function for __init__. Reads log destinations from config and creates them
        
        Example:
        <loggingSettings>
            <logDest severity="warn" dest="@STDERR" />
            <logDest severity="info" source="core" dest="log/core.log" />
            <logDest severity="debug" dest="log/debug.log" />
        </loggingSettings>
        ''' 
        default_ld_cfg = {'severity': 'info', 'source': '.*', 'append': 'False'}
        
        logging_settings = salsa.core.config.get_section('loggerSettings')
        log_dest_cfg = salsa.core.config.process_list(logging_settings, 'logDest', ['dest'], default_ld_cfg)
        
        self.log_dests = [LogDestination(**ldc) for ldc in log_dest_cfg]
    
    def pretty_task_name(self, tname):
        '''Converts python-style thread names (TaskThread-%d) to salsa-style (task-%d)'''
        
        tname = tname.replace('Thread', '')
        tname = tname.lower()
        
        return tname
    
    def log(self, severity, message, tname = None, source = 'core'):
        '''Process log message. May be called from core service (i.e. task manager) and from external 
        client/service using core socket (@see process).
        
        @param severity: severity (@see LoggerSeverity)
        @param message: text of message
        @param tname: thread's name (for in-core calls current_thread.name is used)
        @param source: name of source application or service (for in-core calls - "core")'''
        severity = LoggerSeverity(severity)
        
        if not tname:
            tname = current_thread().name
        
        tname = self.pretty_task_name(tname)
        
        for ld in self.log_dests:
            ld.log(source, tname, severity, message)
            
        if trace_log: do_trace("Log: %s,%s,%s,%s" % (source, tname, severity, message))
            
        return RPCProtocol.STATUS_OK

class Logger:
    '''In-application logger.
    
    Connects to core using IO, and passes messages to LoggerService.
    Uses JSON for encoding IO messages
    
    @note One logger per python thread'''
    def __init__(self, source, core_port, core_host = 'localhost'):
        '''Constructor for logger
        
        Uses thread name from current_thread() 
        
        @param source: Name of source. Usually application or service name
        @param core_port: TCP port of Core 
        @param core_host: Hostname of Core'''
        self.core_port = core_port
        self.core_host = core_host
        
        self.source = source
        self.tname = current_thread().name    
        self.status = None
        
        self.queue = Queue(maxsize = LOG_QUEUE_SIZE)
        
        self.log_slave = None
        
        self.is_done = False
        
    def __del__(self):
        self.stop()
    
    def start(self):
        self.create_slave()
        
        self.log_thread = threading.Thread(target = self._run, 
                                           name = threading._newname('LogThread-%d'))
        self.log_thread.start()
    
    def create_slave(self):
        '''Helper function for __init__ - creates IO slave'''
        self.log_slave = IOSlave(host = self.core_host, port = self.core_port, 
                                    enc_dec = RPCEncoderDecoder())
        self.log_slave.start()
    
    def try_log(self, log_request):
        try:            
            self.log_slave.put(log_request)
            
            reply = self.log_slave.get()
        except IOTransactionError as iote:
            self.log_slave = None
            self.status = 'io transaction exception %s' % str(iote.data)
        except IOException as ioe:
            self.log_slave = None
            self.status = 'io exception %s' % str(ioe)
        except IOSlaveDead:
            self.log_slave = None
            self.status = 'dead slave'
    
    def _run(self):
        while not self.is_done:
            try:
                log_request = self.queue.get(block = True, timeout = LOG_QUEUE_TIMEOUT)
            except Empty:
                continue
            
            self.try_log(log_request)
    
    def log(self, severity, message):
        '''Send log to LoggerService
        
        @raise LoggerException: error condition when sending log message:
            - Bad reply from LoggerService
            - IOSlaveDead (i.e socket is closed)'''
                
        if self.status:
            raise LoggerException('Cannot log request, error "%s"' % self.status)
        
        log_request = {RPCProtocol.KEY_CALL: "log", RPCProtocol.KEY_DATA: {"source": self.source, "tname": self.tname, 
                                                    "severity": severity, "message": message}}
        
        self.queue.put(log_request)
            
    
    def stop(self):
        '''Stops executing of slave'''
        self.is_done = True
        
        if self.log_slave:
            self.log_slave.stop()